﻿using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;

namespace Lr.Core.Versioning
{
    /// <summary>
    /// Defines version information for an <see cref="Assembly"/>.
    /// </summary>
    public sealed class AssemblyVersion : IAssemblyVersion
    {
        #region Construction
        /// <summary>
        /// Stores the version number of the <see cref="Assembly"/>.
        /// </summary>
        private readonly Version _assemblyVersion;

        /// <summary>
        /// Stores version information for the physical file of the <see cref="Assembly"/>.
        /// </summary>
        private readonly FileVersionInfo _assemblyFileVersionInfo;

        /// <summary>
        /// Creates a new <see cref="AssemblyVersion"/> object.
        /// </summary>
        /// <param name="assembly">The <see cref="Assembly"/> to base the <see cref="AssemblyVersion"/> information on.</param>
        /// <exception cref="ArgumentNullException">When <paramref name="assembly"/> is null.</exception>
        public AssemblyVersion(Assembly assembly)
        {
            ThrowIf.Null(() => assembly);

            _assembly = assembly;
            _assemblyVersion = _assembly.GetName().Version;
            _assemblyFileVersionInfo = GetFileVersionInfo(_assembly);
            _buildTimestamp = GetBuildTimestamp(_assembly);
        }
        #endregion Construction

        #region IAssemblyVersion
        /// <summary>
        /// Gets the subject <see cref="Assembly"/> the <see cref="IAssemblyVersion"/> is based on.
        /// </summary>
        public Assembly Assembly { get { return _assembly; } }
        private readonly Assembly _assembly;

        /// <summary>
        /// Gets the major version number.
        /// </summary>
        public int Major
        {
            get
            {
                return
                    _assemblyFileVersionInfo != null
                    ? _assemblyFileVersionInfo.FileMajorPart
                    : _assemblyVersion.Major;
            }
        }

        /// <summary>
        /// Gets the minor version number.
        /// </summary>
        public int Minor
        {
            get
            {
                return
                    _assemblyFileVersionInfo != null
                    ? _assemblyFileVersionInfo.FileMinorPart
                    : _assemblyVersion.Minor;
            }
        }

        /// <summary>
        /// Gets the build version number.
        /// </summary>
        public int Build
        {
            get
            {
                return
                    _assemblyFileVersionInfo != null
                    ? _assemblyFileVersionInfo.FileBuildPart
                    : _assemblyVersion.Build;
            }
        }

        /// <summary>
        /// Gets the <see cref="DateTime"/> when the assembly was built, if available.
        /// </summary>
        public DateTime? BuildTimestamp { get { return _buildTimestamp; } }
        private readonly DateTime? _buildTimestamp;
        #endregion IAssemblyVersion

        #region Private/Internal Methods
        /// <summary>
        /// Gets a <see cref="FileVersionInfo"/> for a given <see cref="Assembly"/>.
        /// </summary>
        /// <param name="assembly">The <see cref="Assembly"/> to get a <see cref="FileVersionInfo"/> object for.</param>
        /// <returns>A <see cref="FileVersionInfo"/> object, or null.</returns>
        private FileVersionInfo GetFileVersionInfo(Assembly assembly)
        {
            if (assembly == null)
                return null;

            if (assembly.IsDynamic)
                return null;

            try {
                var assemblyUncPath = assembly.Location;
                if (!File.Exists(assemblyUncPath))
                    throw new FileNotFoundException("Physical file for Assembly '{0}' could not be found on path '{1}'.".FormatWith(assembly.FullName, assemblyUncPath), assemblyUncPath);

                var fileVersionInfo = FileVersionInfo.GetVersionInfo(assemblyUncPath);
                return fileVersionInfo;
            } catch (Exception ex) {
                var message = "Exception '{0}' occurred while getting a FileVersionInfo object for Assembly '{1}'. Returning 'null' instead of a FileVersionInfo object.".FormatWith(ex.GetType().Name, assembly.FullName);
                Trace.WriteLine(message, "LeRenard.Core.Versioning.AssemblyVersion.GetFileVersionInfo()");
                return null;
            }
        }

        /// <summary>
        /// Gets a <see cref="Nullable"/> <see cref="DateTime"/> object for a given <see cref="Assembly"/> representing the actual time and date of build/compilation.
        /// </summary>
        /// <param name="assembly">The <see cref="Assembly"/></param>
        /// <returns></returns>
        private DateTime? GetBuildTimestamp(Assembly assembly)
        {
            if (assembly == null)
                return null;

            if (assembly.IsDynamic)
                return null;

            try {
                var assemblyUncPath = assembly.Location;
                if (!File.Exists(assemblyUncPath))
                    throw new FileNotFoundException("Physical file for Assembly '{0}' could not be found on path '{1}'.".FormatWith(assembly.FullName, assemblyUncPath), assemblyUncPath);

                const int peHeaderOffset = 60;
                const int linkerTimestampOffset = 8;
                var bytes = new byte[2048];

                using (var fileStream = new FileStream(assemblyUncPath, FileMode.Open, FileAccess.Read)) {
                    fileStream.Read(bytes, 0, 2048);
                }

                var data = BitConverter.ToInt32(bytes, peHeaderOffset);
                var secondsSince1970 = BitConverter.ToInt32(bytes, data + linkerTimestampOffset);
                var timestamp = new DateTime(1970, 1, 1, 0, 0, 0, 0);
                timestamp = timestamp.AddSeconds(secondsSince1970);
                timestamp = timestamp.ToLocalTime();

                return timestamp;
            } catch (Exception ex) {
                var message = "Exception '{0}' occurred while getting a DateTime? object, representing the build timestamp, for Assembly '{1}'. Returning 'null' instead of a FileVersionInfo object.".FormatWith(ex.GetType().Name, assembly.FullName);
                Trace.WriteLine(message);
                return null;
            }
        }
        #endregion Private/Internal Methods

        /// <summary>
        /// Returns a <see cref="String"/> that represents the object.
        /// </summary>
        /// <returns>A <see cref="String"/> that represents the object.</returns>
        public override string ToString()
        {
            return "v{0}.{1}.{2}-{3}".FormatWith(
                    ToStringHelper.For(this.Major),
                    ToStringHelper.For(this.Minor),
                    ToStringHelper.For(this.Build),
                    ToStringHelper.For(this.BuildTimestamp, d => d.Value.ToShortDateString() + "." + d.Value.ToShortTimeString())
                );
        }
    }
}